| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570 |
2x
2x
2x
2x
2x
2x
364x
364x
363x
363x
363x
363x
364x
364x
362x
362x
364x
2x
479x
2x
2x
2x
360x
2x
363x
2x
2x
2x
344x
344x
344x
2x
2x
2x
2x
404x
404x
404x
404x
2x
363x
363x
363x
2x
2x
2x
141x
2x
1359x
2x
780x
2x
2x
363x
2x
56x
2x
2x
2x
563x
563x
2x
2x
2x
2x
2x
2x
423x
423x
423x
423x
423x
423x
2x
2x
2x
2x
2x
2x
287x
287x
2x
2x
2x
2x
362x
362x
362x
362x
2x
363x
363x
363x
363x
363x
362x
362x
362x
362x
362x
362x
362x
362x
362x
362x
2x
| /**
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as api from '../protos/firestore_proto_api';
import { BatchId } from '../core/types';
import { TargetId } from '../core/types';
import { ResourcePath } from '../model/path';
import { assert } from '../util/assert';
import { encode, EncodedResourcePath } from './encoded_resource_path';
import { SimpleDbTransaction } from './simple_db';
import { PersistencePromise } from './persistence_promise';
import { SnapshotVersion } from '../core/snapshot_version';
/**
* Schema Version for the Web client:
* 1. Initial version including Mutation Queue, Query Cache, and Remote Document
* Cache
* 2. Added targetCount to targetGlobal row.
*/
export const SCHEMA_VERSION = 2;
/**
* Performs database creation and schema upgrades.
*
* Note that in production, this method is only ever used to upgrade the schema
* to SCHEMA_VERSION. Different values of toVersion are only used for testing
* and local feature development.
*/
export function createOrUpgradeDb(
db: IDBDatabase,
txn: SimpleDbTransaction,
fromVersion: number,
toVersion: number
): PersistencePromise<void> {
// This function currently supports migrating to schema version 1 (Mutation
// Queue, Query and Remote Document Cache) and schema version 2 (Query
// counting).
assert(
fromVersion < toVersion && fromVersion >= 0 && toVersion <= 2,
'Unexpected schema upgrade from v${fromVersion} to v{toVersion}.'
);
if (fromVersion < 1 && toVersion >= 1) {
createOwnerStore(db);
createMutationQueue(db);
createQueryCache(db);
createRemoteDocumentCache(db);
}
let p = PersistencePromise.resolve();
if (fromVersion < 2 && toVersion >= 2) {
p = ensureTargetGlobalExists(txn).next(targetGlobal =>
saveTargetCount(txn, targetGlobal)
);
}
return p;
}
// TODO(mikelehen): Get rid of "as any" if/when TypeScript fixes their types.
// https://github.com/Microsoft/TypeScript/issues/14322
type KeyPath = any; // tslint:disable-line:no-any
/**
* Wrapper class to store timestamps (seconds and nanos) in IndexedDb objects.
*/
export class DbTimestamp {
constructor(public seconds: number, public nanos: number) {}
}
// The key for the singleton object in the 'owner' store is 'owner'.
export type DbOwnerKey = 'owner';
/**
* A singleton object to be stored in the 'owner' store in IndexedDb.
*
* A given database can be owned by a single tab at a given time. That tab
* must validate that it is still the owner before every write operation and
* should regularly write an updated timestamp to prevent other tabs from
* "stealing" ownership of the db.
*/
export class DbOwner {
/** Name of the IndexedDb object store. */
static store = 'owner';
constructor(public ownerId: string, public leaseTimestampMs: number) {}
}
function createOwnerStore(db: IDBDatabase): void {
db.createObjectStore(DbOwner.store);
}
/** Object keys in the 'mutationQueues' store are userId strings. */
export type DbMutationQueueKey = string;
/**
* An object to be stored in the 'mutationQueues' store in IndexedDb.
*
* Each user gets a single queue of MutationBatches to apply to the server.
* DbMutationQueue tracks the metadata about the queue.
*/
export class DbMutationQueue {
/** Name of the IndexedDb object store. */
static store = 'mutationQueues';
/** Keys are automatically assigned via the userId property. */
static keyPath = 'userId';
constructor(
/**
* The normalized user ID to which this queue belongs.
*/
public userId: string,
/**
* An identifier for the highest numbered batch that has been acknowledged
* by the server. All MutationBatches in this queue with batchIds less
* than or equal to this value are considered to have been acknowledged by
* the server.
*/
public lastAcknowledgedBatchId: number,
/**
* A stream token that was previously sent by the server.
*
* See StreamingWriteRequest in datastore.proto for more details about
* usage.
*
* After sending this token, earlier tokens may not be used anymore so
* only a single stream token is retained.
*/
public lastStreamToken: string
) {}
}
/** keys in the 'mutations' object store are [userId, batchId] pairs. */
export type DbMutationBatchKey = [string, BatchId];
/**
* An object to be stored in the 'mutations' store in IndexedDb.
*
* Represents a batch of user-level mutations intended to be sent to the server
* in a single write. Each user-level batch gets a separate DbMutationBatch
* with a new batchId.
*/
export class DbMutationBatch {
/** Name of the IndexedDb object store. */
static store = 'mutations';
/** Keys are automatically assigned via the userId, batchId properties. */
static keyPath = ['userId', 'batchId'];
constructor(
/**
* The normalized user ID to which this batch belongs.
*/
public userId: string,
/**
* An identifier for this batch, allocated by the mutation queue in a
* monotonically increasing manner.
*/
public batchId: BatchId,
/**
* The local write time of the batch, stored as milliseconds since the
* epoch.
*/
public localWriteTimeMs: number,
/**
* A list of mutations to apply. All mutations will be applied atomically.
*
* Mutations are serialized via JsonProtoSerializer.toMutation().
*/
public mutations: api.Write[]
) {}
}
/**
* The key for a db document mutation, which is made up of a userID, path, and
* batchId. Note that the path must be serialized into a form that indexedDB can
* sort.
*/
export type DbDocumentMutationKey = [string, EncodedResourcePath, BatchId];
function createMutationQueue(db: IDBDatabase): void {
db.createObjectStore(DbMutationQueue.store, {
keyPath: DbMutationQueue.keyPath
});
db.createObjectStore(DbMutationBatch.store, {
keyPath: DbMutationBatch.keyPath as KeyPath
});
db.createObjectStore(DbDocumentMutation.store);
}
/**
* An object to be stored in the 'documentMutations' store in IndexedDb.
*
* A manually maintained index of all the mutation batches that affect a given
* document key. The rows in this table are references based on the contents of
* DbMutationBatch.mutations.
*/
export class DbDocumentMutation {
static store = 'documentMutations';
/**
* Creates a [userId] key for use in the DbDocumentMutations index to iterate
* over all of a user's document mutations.
*/
static prefixForUser(userId: string): [string] {
return [userId];
}
/**
* Creates a [userId, encodedPath] key for use in the DbDocumentMutations
* index to iterate over all at document mutations for a given path or lower.
*/
static prefixForPath(
userId: string,
path: ResourcePath
): [string, EncodedResourcePath] {
return [userId, encode(path)];
}
/**
* Creates a full index key of [userId, encodedPath, batchId] for inserting
* and deleting into the DbDocumentMutations index.
*/
static key(
userId: string,
path: ResourcePath,
batchId: BatchId
): DbDocumentMutationKey {
return [userId, encode(path), batchId];
}
/**
* Because we store all the useful information for this store in the key,
* there is no useful information to store as the value. The raw (unencoded)
* path cannot be stored because IndexedDb doesn't store prototype
* information.
*/
static PLACEHOLDER = new DbDocumentMutation();
private constructor() {}
}
/**
* A key in the 'remoteDocuments' object store is a string array containing the
* segments that make up the path.
*/
export type DbRemoteDocumentKey = string[];
function createRemoteDocumentCache(db: IDBDatabase): void {
db.createObjectStore(DbRemoteDocument.store);
}
/**
* Represents the known absence of a document at a particular version.
* Stored in IndexedDb as part of a DbRemoteDocument object.
*/
export class DbNoDocument {
constructor(public path: string[], public readTime: DbTimestamp) {}
}
/**
* An object to be stored in the 'remoteDocuments' store in IndexedDb. It
* represents either a cached document (if it exists) or a cached "no-document"
* (if it is known to not exist).
*
* Note: This is the persisted equivalent of a MaybeDocument and could perhaps
* be made more general if necessary.
*/
export class DbRemoteDocument {
static store = 'remoteDocuments';
constructor(
/**
* Set to an instance of a DbNoDocument if it is known that no document
* exists.
*/
public noDocument: DbNoDocument | null,
/**
* Set to an instance of a Document if there's a cached version of the
* document.
*/
public document: api.Document | null
) {}
}
/**
* A key in the 'targets' object store is a targetId of the query.
*/
export type DbTargetKey = TargetId;
/**
* The persisted type for a query nested with in the 'targets' store in
* IndexedDb. We use the proto definitions for these two kinds of queries in
* order to avoid writing extra serialization logic.
*/
export type DbQuery = api.QueryTarget | api.DocumentsTarget;
/**
* An object to be stored in the 'targets' store in IndexedDb.
*
* This is based on and should be kept in sync with the proto used in the iOS
* client.
*
* Each query the client listens to against the server is tracked on disk so
* that the query can be efficiently resumed on restart.
*/
export class DbTarget {
static store = 'targets';
/** Keys are automatically assigned via the targetId property. */
static keyPath = 'targetId';
/** The name of the queryTargets index. */
static queryTargetsIndexName = 'queryTargetsIndex';
/**
* The index of all canonicalIds to the targets that they match. This is not
* a unique mapping because canonicalId does not promise a unique name for all
* possible queries, so we append the targetId to make the mapping unique.
*/
static queryTargetsKeyPath = ['canonicalId', 'targetId'];
constructor(
/**
* An auto-generated sequential numeric identifier for the query.
*
* Queries are stored using their canonicalId as the key, but these
* canonicalIds can be quite long so we additionally assign a unique
* queryId which can be used by referenced data structures (e.g.
* indexes) to minimize the on-disk cost.
*/
public targetId: TargetId,
/**
* The canonical string representing this query. This is not unique.
*/
public canonicalId: string,
/**
* The last readTime received from the Watch Service for this query.
*
* This is the same value as TargetChange.read_time in the protos.
*/
public readTime: DbTimestamp,
/**
* An opaque, server-assigned token that allows watching a query to be
* resumed after disconnecting without retransmitting all the data
* that matches the query. The resume token essentially identifies a
* point in time from which the server should resume sending results.
*
* This is related to the snapshotVersion in that the resumeToken
* effectively also encodes that value, but the resumeToken is opaque
* and sometimes encodes additional information.
*
* A consequence of this is that the resumeToken should be used when
* asking the server to reason about where this client is in the watch
* stream, but the client should use the snapshotVersion for its own
* purposes.
*
* This is the same value as TargetChange.resume_token in the protos.
*/
public resumeToken: string,
/**
* A sequence number representing the last time this query was
* listened to, used for garbage collection purposes.
*
* Conventionally this would be a timestamp value, but device-local
* clocks are unreliable and they must be able to create new listens
* even while disconnected. Instead this should be a monotonically
* increasing number that's incremented on each listen call.
*
* This is different from the queryId since the queryId is an
* immutable identifier assigned to the Query on first use while
* lastListenSequenceNumber is updated every time the query is
* listened to.
*/
public lastListenSequenceNumber: number,
/**
* The query for this target.
*
* Because canonical ids are not unique we must store the actual query. We
* use the proto to have an object we can persist without having to
* duplicate translation logic to and from a `Query` object.
*/
public query: DbQuery
) {}
}
/**
* The key for a DbTargetDocument, containing a targetId and an encoded resource
* path.
*/
export type DbTargetDocumentKey = [TargetId, EncodedResourcePath];
/**
* An object representing an association between a target and a document.
* Stored in the targetDocument object store to store the documents tracked by a
* particular target.
*/
export class DbTargetDocument {
/** Name of the IndexedDb object store. */
static store = 'targetDocuments';
/** Keys are automatically assigned via the targetId, path properties. */
static keyPath = ['targetId', 'path'];
/** The index name for the reverse index. */
static documentTargetsIndex = 'documentTargetsIndex';
/** We also need to create the reverse index for these properties. */
static documentTargetsKeyPath = ['path', 'targetId'];
constructor(
/**
* The targetId identifying a target.
*/
public targetId: TargetId,
/**
* The path to the document, as encoded in the key.
*/
public path: EncodedResourcePath
) {}
}
/**
* The type to represent the single allowed key for the DbTargetGlobal store.
*/
export type DbTargetGlobalKey = typeof DbTargetGlobal.key;
/**
* A record of global state tracked across all Targets, tracked separately
* to avoid the need for extra indexes.
*
* This should be kept in-sync with the proto used in the iOS client.
*/
export class DbTargetGlobal {
/**
* The key string used for the single object that exists in the
* DbTargetGlobal store.
*/
static key = 'targetGlobalKey';
static store = 'targetGlobal';
constructor(
/**
* The highest numbered target id across all targets.
*
* See DbTarget.targetId.
*/
public highestTargetId: TargetId,
/**
* The highest numbered lastListenSequenceNumber across all targets.
*
* See DbTarget.lastListenSequenceNumber.
*/
public highestListenSequenceNumber: number,
/**
* A global snapshot version representing the last consistent snapshot we
* received from the backend. This is monotonically increasing and any
* snapshots received from the backend prior to this version (e.g. for
* targets resumed with a resumeToken) should be suppressed (buffered)
* until the backend has caught up to this snapshot version again. This
* prevents our cache from ever going backwards in time.
*/
public lastRemoteSnapshotVersion: DbTimestamp,
/**
* The number of targets persisted.
*/
public targetCount: number
) {}
}
function createQueryCache(db: IDBDatabase): void {
const targetDocumentsStore = db.createObjectStore(DbTargetDocument.store, {
keyPath: DbTargetDocument.keyPath as KeyPath
});
targetDocumentsStore.createIndex(
DbTargetDocument.documentTargetsIndex,
DbTargetDocument.documentTargetsKeyPath,
{ unique: true }
);
const targetStore = db.createObjectStore(DbTarget.store, {
keyPath: DbTarget.keyPath
});
// NOTE: This is unique only because the TargetId is the suffix.
targetStore.createIndex(
DbTarget.queryTargetsIndexName,
DbTarget.queryTargetsKeyPath,
{ unique: true }
);
db.createObjectStore(DbTargetGlobal.store);
}
/**
* Counts the number of targets persisted and adds that value to the target
* global singleton.
*/
function saveTargetCount(
txn: SimpleDbTransaction,
metadata: DbTargetGlobal
): PersistencePromise<void> {
const globalStore = txn.store<DbTargetGlobalKey, DbTargetGlobal>(
DbTargetGlobal.store
);
const targetStore = txn.store<DbTargetKey, DbTarget>(DbTarget.store);
return targetStore.count().next(count => {
metadata.targetCount = count;
return globalStore.put(DbTargetGlobal.key, metadata);
});
}
/**
* Ensures that the target global singleton row exists by adding it if it's
* missing.
*
* @param {IDBTransaction} txn The version upgrade transaction for indexeddb
*/
function ensureTargetGlobalExists(
txn: SimpleDbTransaction
): PersistencePromise<DbTargetGlobal> {
const globalStore = txn.store<DbTargetGlobalKey, DbTargetGlobal>(
DbTargetGlobal.store
);
return globalStore.get(DbTargetGlobal.key).next(metadata => {
Iif (metadata != null) {
return PersistencePromise.resolve(metadata);
} else {
metadata = new DbTargetGlobal(
/*highestTargetId=*/ 0,
/*lastListenSequenceNumber=*/ 0,
SnapshotVersion.MIN.toTimestamp(),
/*targetCount=*/ 0
);
return globalStore.put(DbTargetGlobal.key, metadata).next(() => metadata);
}
});
}
/**
* The list of all default IndexedDB stores used throughout the SDK. This is
* used when creating transactions so that access across all stores is done
* atomically.
*/
export const ALL_STORES = [
DbMutationQueue.store,
DbMutationBatch.store,
DbDocumentMutation.store,
DbRemoteDocument.store,
DbTarget.store,
DbOwner.store,
DbTargetGlobal.store,
DbTargetDocument.store
];
|